//
//  KDCircularProgress.swift
//  KDCircularProgress
//
//  Copyright (c) 2019 Kaan Dedeoglu. All rights reserved.
//

import UIKit

@objcMembers
public class KDCircularProgress: UIView, CAAnimationDelegate {
    private var progressLayer: KDCircularProgressViewLayer {
        get {
            return layer as! KDCircularProgressViewLayer
        }
    }
    
    private var radius: CGFloat = 0.0 {
        didSet {
            progressLayer.radius = radius
        }
    }
    
    public var angle: Double = 0.0 {
        didSet {
            progressLayer.angle = angle
        }
    }
    
    public var startAngle: Double = 0.0 {
        didSet {
            startAngle = startAngle.mod(between: 0.0, and: 360.0, byIncrementing: 360.0)
            progressLayer.startAngle = startAngle
        }
    }
        
    public var progressThickness: CGFloat = 0.5
    
    public var trackThickness: CGFloat = 0.5
    
    public var trackColor: UIColor = .black {
        didSet {
            progressLayer.trackColor = trackColor
        }
    }
    
    public var progressInsideFillColor: UIColor? = nil {
        didSet {
            progressLayer.progressInsideFillColor = progressInsideFillColor ?? .clear
        }
    }
        
    private var animationCompletionBlock: ((Bool) -> Void)?
    
    override public init(frame: CGRect) {
        super.init(frame: frame)
        setInitialValues()
        refreshValues()
    }
    
    convenience public init(frame:CGRect, colors: UIColor...) {
        self.init(frame: frame)
        set(colors: colors)
    }
    
    required public init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
    
    override public class var layerClass: AnyClass {
        return KDCircularProgressViewLayer.self
    }
    
    public override func layoutSubviews() {
        super.layoutSubviews()
        radius = (frame.size.width / 2.0) * 0.8
    }
    
    private func setInitialValues() {
        radius = (frame.size.width / 2.0) * 0.8 //We always apply a 20% padding, stopping glows from being clipped
        backgroundColor = .clear
        set(colors: .white, .cyan)
    }
    
    private func refreshValues() {
        progressLayer.angle = angle
        progressLayer.startAngle = startAngle
        progressLayer.progressThickness = progressThickness / 2.0
        progressLayer.trackColor = trackColor
        progressLayer.trackThickness = trackThickness / 2.0
    }
        
    public func set(colors: UIColor...) {
        set(colors: colors)
    }
    
    private func set(colors: [UIColor]) {
        progressLayer.colorsArray = colors
        progressLayer.setNeedsDisplay()
    }
    
    public func pauseAnimation() {
        guard let presentationLayer = progressLayer.presentation() else { return }
        
        let currentValue = presentationLayer.angle
        progressLayer.removeAllAnimations()
        angle = currentValue
    }
}

class KDCircularProgressViewLayer: CALayer {
    @NSManaged var angle: Double
    var radius: CGFloat = 0.0
    var startAngle: Double = 0.0
    var progressThickness: CGFloat = 0.5
    var trackThickness: CGFloat = 0.5
    var trackColor: UIColor = .lightGray
    var progressInsideFillColor: UIColor = .clear
    var colorsArray: [UIColor] = []
    private var gradientCache: CGGradient?
    private var locationsCache: [CGFloat]?
    
    override class func needsDisplay(forKey key: String) -> Bool {
        if key == #keyPath(angle) {
            return true
        }
        return super.needsDisplay(forKey: key)
    }
    
    override init(layer: Any) {
        super.init(layer: layer)
        let progressLayer = layer as! KDCircularProgressViewLayer
        radius = progressLayer.radius
        angle = progressLayer.angle
        startAngle = progressLayer.startAngle
        progressThickness = progressLayer.progressThickness
        trackThickness = progressLayer.trackThickness
        trackColor = progressLayer.trackColor
        colorsArray = progressLayer.colorsArray
        progressInsideFillColor = progressLayer.progressInsideFillColor
    }
    
    override init() {
        super.init()
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
    
    override func draw(in ctx: CGContext) {
        UIGraphicsPushContext(ctx)
        
        let size = bounds.size
        let width = size.width
        let height = size.height
        
        let trackLineWidth = radius * trackThickness
        let progressLineWidth = radius * progressThickness
        let arcRadius = max(radius - trackLineWidth / 2.0, radius - progressLineWidth / 2.0)
        ctx.addArc(center: CGPoint(x: width / 2.0, y: height / 2.0),
                   radius: arcRadius,
                   startAngle: CGFloat.pi * 1 / 6,///CGFloat.pi * 5 / 4,
                   endAngle: CGFloat.pi * 5 / 6,
                   clockwise: true)
        ctx.setStrokeColor(trackColor.cgColor)
        ctx.setFillColor(progressInsideFillColor.cgColor)
        ctx.setLineWidth(trackLineWidth)
        ctx.setLineCap(.round)
        ctx.drawPath(using: .fillStroke)
        UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
        
        let imageCtx = UIGraphicsGetCurrentContext()
        let canonicalAngle = angle.mod(between: 0.0, and: 270.0, byIncrementing: 270)
        let fromAngle = -startAngle.radians
        let toAngle = (-canonicalAngle - startAngle).radians
        imageCtx?.addArc(center: CGPoint(x: width / 2.0, y: height / 2.0),
                         radius: arcRadius,
                         startAngle: CGFloat(fromAngle),
                         endAngle: CGFloat(toAngle),
                         clockwise: true)
        imageCtx?.setLineCap(.round)
        imageCtx?.setLineWidth(progressLineWidth)
        imageCtx?.drawPath(using: .stroke)
        
        let drawMask: CGImage = UIGraphicsGetCurrentContext()!.makeImage()!
        UIGraphicsEndImageContext()
        
        ctx.saveGState()
        ctx.clip(to: bounds, mask: drawMask)
        drawGradient(withContext: ctx, colorsArray: colorsArray)
        ctx.restoreGState()
        UIGraphicsPopContext()
    }

    private func drawGradient(withContext context: CGContext, colorsArray: [UIColor]) {
        let baseSpace = CGColorSpaceCreateDeviceRGB()
        let locations = locationsCache ?? gradientLocationsFor(colorCount: colorsArray.count, gradientWidth: bounds.size.width)
        let gradient: CGGradient
        
        if let cachedGradient = gradientCache {
            gradient = cachedGradient
        } else {
            guard let newGradient = CGGradient(colorSpace: baseSpace, colorComponents: colorsArray.rgbNormalized.componentsJoined,
                                               locations: locations, count: colorsArray.count) else { return }
            
            gradientCache = newGradient
            gradient = newGradient
        }
        
        let halfX = bounds.size.width / 2.0
        let floatPi = CGFloat.pi
        let rotateSpeed = 0.0///gradientRotateSpeed//clockwise == true ? gradientRotateSpeed : gradientRotateSpeed * -1.0
        let angleInRadians = (rotateSpeed * CGFloat(angle) - 90.0).radians
        let oppositeAngle = angleInRadians > floatPi ? angleInRadians - floatPi : angleInRadians + floatPi
        
        let startPoint = CGPoint(x: (cos(angleInRadians) * halfX) + halfX, y: (sin(angleInRadians) * halfX) + halfX)
        let endPoint = CGPoint(x: (cos(oppositeAngle) * halfX) + halfX, y: (sin(oppositeAngle) * halfX) + halfX)
        
        context.drawLinearGradient(gradient, start: startPoint, end: endPoint, options: .drawsBeforeStartLocation)
    }
    
    private func gradientLocationsFor(colorCount: Int, gradientWidth: CGFloat) -> [CGFloat] {
        guard colorCount > 0, gradientWidth > 0 else { return [] }

        let progressLineWidth = radius * progressThickness
        let firstPoint = gradientWidth / 2.0 - (radius - progressLineWidth / 2.0)
        let increment = (gradientWidth - (2.0 * firstPoint)) / CGFloat(colorCount - 1)
        
        let locationsArray = (0..<colorCount).map { firstPoint + (CGFloat($0) * increment) }
        let result = locationsArray.map { $0 / gradientWidth }
        locationsCache = result
        return result
    }
}

private extension Array where Element == UIColor {
    // Make sure every color in colors array is in RGB color space
    var rgbNormalized: [UIColor] {
        return map { color in
            guard color.cgColor.numberOfComponents == 2 else {
                return color
            }
            
            let white: CGFloat = color.cgColor.components![0]
            return UIColor(red: white, green: white, blue: white, alpha: 1.0)
        }
    }
    
    var componentsJoined: [CGFloat] {
        return flatMap { $0.cgColor.components ?? [] }
    }
}

private extension FloatingPoint {
    var radians: Self {
        return self * .pi / Self(180)
    }
    
    func mod(between left: Self, and right: Self, byIncrementing interval: Self) -> Self {
        if self >= left, self <= right {
            return self
        } else if self < left {
            return (self + interval).mod(between: left, and: right, byIncrementing: interval)
        } else {
            return (self - interval).mod(between: left, and: right, byIncrementing: interval)
        }
    }
}
